/*
 * File: iframeResizer.js
 * Desc: Force iframes to size to content.
 * Requires: iframeResizer.contentWindow.js to be loaded into the target frame.
 * Doc: https://github.com/davidjbradshaw/iframe-resizer
 * Author: David J. Bradshaw - dave@bradshaw.net
 * Contributor: Jure Mav - jure.mav@gmail.com
 * Contributor: Reed Dadoune - reed@dadoune.com
 */


;(function(undefined) {
	'use strict';

	if(typeof window === 'undefined') return; // don't run for server side render

	var
		count                 = 0,
		logEnabled            = false,
		hiddenCheckEnabled    = false,
		msgHeader             = 'message',
		msgHeaderLen          = msgHeader.length,
		msgId                 = '[iFrameSizer]', //Must match iframe msg ID
		msgIdLen              = msgId.length,
		pagePosition          = null,
		requestAnimationFrame = window.requestAnimationFrame,
		resetRequiredMethods  = {max:1,scroll:1,bodyScroll:1,documentElementScroll:1},
		settings              = {},
		timer                 = null,
		logId                 = 'Host Page',

		defaults              = {
			autoResize                : true,
			bodyBackground            : null,
			bodyMargin                : null,
			bodyMarginV1              : 8,
			bodyPadding               : null,
			checkOrigin               : true,
			inPageLinks               : false,
			enablePublicMethods       : true,
			heightCalculationMethod   : 'bodyOffset',
			id                        : 'iFrameResizer',
			interval                  : 32,
			log                       : false,
			maxHeight                 : Infinity,
			maxWidth                  : Infinity,
			minHeight                 : 0,
			minWidth                  : 0,
			resizeFrom                : 'parent',
			scrolling                 : false,
			sizeHeight                : true,
			sizeWidth                 : false,
			warningTimeout            : 5000,
			tolerance                 : 0,
			widthCalculationMethod    : 'scroll',
			closedCallback            : function(){},
			initCallback              : function(){},
			messageCallback           : function(){warn('MessageCallback function not defined');},
			resizedCallback           : function(){},
			scrollCallback            : function(){return true;}
		};

	function addEventListener(obj,evt,func){
		/* istanbul ignore else */ // Not testable in PhantonJS
		if ('addEventListener' in window){
			obj.addEventListener(evt,func, false);
		} else if ('attachEvent' in window){//IE
			obj.attachEvent('on'+evt,func);
		}
	}

	function removeEventListener(el,evt,func){
		/* istanbul ignore else */ // Not testable in phantonJS
		if ('removeEventListener' in window){
			el.removeEventListener(evt,func, false);
		} else if ('detachEvent' in window){ //IE
			el.detachEvent('on'+evt,func);
		}
	}

	function setupRequestAnimationFrame(){
		var
			vendors = ['moz', 'webkit', 'o', 'ms'],
			x;

		// Remove vendor prefixing if prefixed and break early if not
		for (x = 0; x < vendors.length && !requestAnimationFrame; x += 1) {
			requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
		}

		if (!(requestAnimationFrame)){
			log('setup','RequestAnimationFrame not supported');
		}
	}

	function getMyID(iframeId){
		var retStr = 'Host page: '+iframeId;

		if (window.top !== window.self){
			if (window.parentIFrame && window.parentIFrame.getId){
				retStr = window.parentIFrame.getId()+': '+iframeId;
			} else {
				retStr = 'Nested host page: '+iframeId;
			}
		}

		return retStr;
	}

	function formatLogHeader(iframeId){
		return msgId + '[' + getMyID(iframeId) + ']';
	}

	function isLogEnabled(iframeId){
		return settings[iframeId] ? settings[iframeId].log : logEnabled;
	}

	function log(iframeId,msg){
		output('log',iframeId,msg,isLogEnabled(iframeId));
	}

	function info(iframeId,msg){
		output('info',iframeId,msg,isLogEnabled(iframeId));
	}

	function warn(iframeId,msg){
		output('warn',iframeId,msg,true);
	}

	function output(type,iframeId,msg,enabled){
		if (true === enabled && 'object' === typeof window.console){
			console[type](formatLogHeader(iframeId),msg);
		}
	}

	function iFrameListener(event){
		function resizeIFrame(){
			function resize(){
				setSize(messageData);
				setPagePosition(iframeId);
			}

			ensureInRange('Height');
			ensureInRange('Width');

			syncResize(resize,messageData,'init');
		}

		function processMsg(){
			var data = msg.substr(msgIdLen).split(':');

			return {
				iframe: settings[data[0]].iframe,
				id:     data[0],
				height: data[1],
				width:  data[2],
				type:   data[3]
			};
		}

		function ensureInRange(Dimension){
			var
				max  = Number(settings[iframeId]['max' + Dimension]),
				min  = Number(settings[iframeId]['min' + Dimension]),
				dimension = Dimension.toLowerCase(),
				size = Number(messageData[dimension]);

			log(iframeId,'Checking ' + dimension + ' is in range ' + min + '-' + max);

			if (size<min) {
				size=min;
				log(iframeId,'Set ' + dimension + ' to min value');
			}

			if (size>max) {
				size=max;
				log(iframeId,'Set ' + dimension + ' to max value');
			}

			messageData[dimension] = '' + size;
		}


		function isMessageFromIFrame(){
			function checkAllowedOrigin(){
				function checkList(){
					var
						i = 0,
						retCode = false;

					log(iframeId,'Checking connection is from allowed list of origins: ' + checkOrigin);

					for (; i < checkOrigin.length; i++) {
						if (checkOrigin[i] === origin) {
							retCode = true;
							break;
						}
					}
					return retCode;
				}

				function checkSingle(){
					var remoteHost  = settings[iframeId].remoteHost;
					log(iframeId,'Checking connection is from: '+remoteHost);
					return origin === remoteHost;
				}

				return checkOrigin.constructor === Array ? checkList() : checkSingle();
			}

			var
				origin      = event.origin,
				checkOrigin = settings[iframeId].checkOrigin;

			if (checkOrigin && (''+origin !== 'null') && !checkAllowedOrigin()) {
				throw new Error(
					'Unexpected message received from: ' + origin +
					' for ' + messageData.iframe.id +
					'. Message was: ' + event.data +
					'. This error can be disabled by setting the checkOrigin: false option or by providing of array of trusted domains.'
				);
			}

			return true;
		}

		function isMessageForUs(){
			return msgId === (('' + msg).substr(0,msgIdLen)) && (msg.substr(msgIdLen).split(':')[0] in settings); //''+Protects against non-string msg
		}

		function isMessageFromMetaParent(){
			//Test if this message is from a parent above us. This is an ugly test, however, updating
			//the message format would break backwards compatibity.
			var retCode = messageData.type in {'true':1,'false':1,'undefined':1};

			if (retCode){
				log(iframeId,'Ignoring init message from meta parent page');
			}

			return retCode;
		}

		function getMsgBody(offset){
			return msg.substr(msg.indexOf(':')+msgHeaderLen+offset);
		}

		function forwardMsgFromIFrame(msgBody){
			log(iframeId,'MessageCallback passed: {iframe: '+ messageData.iframe.id + ', message: ' + msgBody + '}');
			callback('messageCallback',{
				iframe: messageData.iframe,
				message: JSON.parse(msgBody)
			});
			log(iframeId,'--');
		}

		function getPageInfo(){
			var
				bodyPosition   = document.body.getBoundingClientRect(),
				iFramePosition = messageData.iframe.getBoundingClientRect();

			return JSON.stringify({
				iframeHeight: iFramePosition.height,
				iframeWidth:  iFramePosition.width,
				clientHeight: Math.max(document.documentElement.clientHeight, window.innerHeight || 0),
				clientWidth:  Math.max(document.documentElement.clientWidth,  window.innerWidth  || 0),
				offsetTop:    parseInt(iFramePosition.top  - bodyPosition.top,  10),
				offsetLeft:   parseInt(iFramePosition.left - bodyPosition.left, 10),
				scrollTop:    window.pageYOffset,
				scrollLeft:   window.pageXOffset
			});
		}

		function sendPageInfoToIframe(iframe,iframeId){
			function debouncedTrigger(){
				trigger(
					'Send Page Info',
					'pageInfo:' + getPageInfo(),
					iframe,
					iframeId
				);
			}

			debouce(debouncedTrigger,32);
		}


		function startPageInfoMonitor(){
			function setListener(type,func){
				function sendPageInfo(){
					if (settings[id]){
						sendPageInfoToIframe(settings[id].iframe,id);
					} else {
						stop();
					}
				}

				['scroll','resize'].forEach(function(evt){
					log(id, type +  evt + ' listener for sendPageInfo');
					func(window,evt,sendPageInfo);
				});
			}

			function stop(){
				setListener('Remove ', removeEventListener);
			}

			function start(){
				setListener('Add ', addEventListener);
			}

			var id = iframeId; //Create locally scoped copy of iFrame ID

			start();

			settings[id].stopPageInfo = stop;
		}

		function stopPageInfoMonitor(){
			if (settings[iframeId] && settings[iframeId].stopPageInfo){
				settings[iframeId].stopPageInfo();
				delete settings[iframeId].stopPageInfo;
			}
		}

		function checkIFrameExists(){
			var retBool = true;

			if (null === messageData.iframe) {
				warn(iframeId,'IFrame ('+messageData.id+') not found');
				retBool = false;
			}
			return retBool;
		}

		function getElementPosition(target){
			var iFramePosition = target.getBoundingClientRect();

			getPagePosition(iframeId);

			return {
				x: Math.floor( Number(iFramePosition.left) + Number(pagePosition.x) ),
				y: Math.floor( Number(iFramePosition.top)  + Number(pagePosition.y) )
			};
		}

		function scrollRequestFromChild(addOffset){
			/* istanbul ignore next */  //Not testable in Karma
			function reposition(){
				pagePosition = newPosition;
				scrollTo();
				log(iframeId,'--');
			}

			function calcOffset(){
				return {
					x: Number(messageData.width) + offset.x,
					y: Number(messageData.height) + offset.y
				};
			}

			function scrollParent(){
				if (window.parentIFrame){
					window.parentIFrame['scrollTo'+(addOffset?'Offset':'')](newPosition.x,newPosition.y);
				} else {
					warn(iframeId,'Unable to scroll to requested position, window.parentIFrame not found');
				}
			}

			var
				offset = addOffset ? getElementPosition(messageData.iframe) : {x:0,y:0},
				newPosition = calcOffset();

			log(iframeId,'Reposition requested from iFrame (offset x:'+offset.x+' y:'+offset.y+')');

			if(window.top !== window.self){
				scrollParent();
			} else {
				reposition();
			}
		}

		function scrollTo(){
			if (false !== callback('scrollCallback',pagePosition)){
				setPagePosition(iframeId);
			} else {
				unsetPagePosition();
			}
		}

		function findTarget(location){
			function jumpToTarget(){
				var jumpPosition = getElementPosition(target);

				log(iframeId,'Moving to in page link (#'+hash+') at x: '+jumpPosition.x+' y: '+jumpPosition.y);
				pagePosition = {
					x: jumpPosition.x,
					y: jumpPosition.y
				};

				scrollTo();
				log(iframeId,'--');
			}

			function jumpToParent(){
				if (window.parentIFrame){
					window.parentIFrame.moveToAnchor(hash);
				} else {
					log(iframeId,'In page link #'+hash+' not found and window.parentIFrame not found');
				}
			}

			var
				hash     = location.split('#')[1] || '',
				hashData = decodeURIComponent(hash),
				target   = document.getElementById(hashData) || document.getElementsByName(hashData)[0];

			if (target){
				jumpToTarget();
			} else if(window.top!==window.self){
				jumpToParent();
			} else {
				log(iframeId,'In page link #'+hash+' not found');
			}
		}

		function callback(funcName,val){
			return chkCallback(iframeId,funcName,val);
		}

		function actionMsg(){

			if(settings[iframeId].firstRun) firstRun();

			switch(messageData.type){
			case 'close':
				closeIFrame(messageData.iframe);
				break;
			case 'message':
				forwardMsgFromIFrame(getMsgBody(6));
				break;
			case 'scrollTo':
				scrollRequestFromChild(false);
				break;
			case 'scrollToOffset':
				scrollRequestFromChild(true);
				break;
			case 'pageInfo':
				sendPageInfoToIframe(settings[iframeId].iframe,iframeId);
				startPageInfoMonitor();
				break;
			case 'pageInfoStop':
				stopPageInfoMonitor();
				break;
			case 'inPageLink':
				findTarget(getMsgBody(9));
				break;
			case 'reset':
				resetIFrame(messageData);
				break;
			case 'init':
				resizeIFrame();
				callback('initCallback',messageData.iframe);
				callback('resizedCallback',messageData);
				break;
			default:
				resizeIFrame();
				callback('resizedCallback',messageData);
			}
		}

		function hasSettings(iframeId){
			var retBool = true;

			if (!settings[iframeId]){
				retBool = false;
				warn(messageData.type + ' No settings for ' + iframeId + '. Message was: ' + msg);
			}

			return retBool;
		}

		function iFrameReadyMsgReceived(){
			for (var iframeId in settings){
				trigger('iFrame requested init',createOutgoingMsg(iframeId),document.getElementById(iframeId),iframeId);
			}
		}

		function firstRun() {
			settings[iframeId].firstRun = false;
		}

		var
			msg = event.data,
			messageData = {},
			iframeId = null;

		if('[iFrameResizerChild]Ready' === msg){
			iFrameReadyMsgReceived();
		} else if (isMessageForUs()){
			messageData = processMsg();
			iframeId    = logId = messageData.id;

			clearTimeout(settings[iframeId].msgTimeout);

			if (!isMessageFromMetaParent() && hasSettings(iframeId)){
				log(iframeId,'Received: '+msg);

				if ( checkIFrameExists() && isMessageFromIFrame() ){
					actionMsg();
				}
			}
		} else {
			info(iframeId,'Ignored: '+msg);
		}

	}


	function chkCallback(iframeId,funcName,val){
		var
			func = null,
			retVal = null;

		if(settings[iframeId]){
			func = settings[iframeId][funcName];

			if( 'function' === typeof func){
				retVal = func(val);
			} else {
				throw new TypeError(funcName+' on iFrame['+iframeId+'] is not a function');
			}
		}

		return retVal;
	}

	function closeIFrame(iframe){
		var iframeId = iframe.id;

		log(iframeId,'Removing iFrame: '+iframeId);
		if (iframe.parentNode) { iframe.parentNode.removeChild(iframe); }
		chkCallback(iframeId,'closedCallback',iframeId);
		log(iframeId,'--');
		delete settings[iframeId];
	}

	function getPagePosition(iframeId){
		if(null === pagePosition){
			pagePosition = {
				x: (window.pageXOffset !== undefined) ? window.pageXOffset : document.documentElement.scrollLeft,
				y: (window.pageYOffset !== undefined) ? window.pageYOffset : document.documentElement.scrollTop
			};
			log(iframeId,'Get page position: '+pagePosition.x+','+pagePosition.y);
		}
	}

	function setPagePosition(iframeId){
		if(null !== pagePosition){
			window.scrollTo(pagePosition.x,pagePosition.y);
			log(iframeId,'Set page position: '+pagePosition.x+','+pagePosition.y);
			unsetPagePosition();
		}
	}

	function unsetPagePosition(){
		pagePosition = null;
	}

	function resetIFrame(messageData){
		function reset(){
			setSize(messageData);
			trigger('reset','reset',messageData.iframe,messageData.id);
		}

		log(messageData.id,'Size reset requested by '+('init'===messageData.type?'host page':'iFrame'));
		getPagePosition(messageData.id);
		syncResize(reset,messageData,'reset');
	}

	function setSize(messageData){
		function setDimension(dimension){
			messageData.iframe.style[dimension] = messageData[dimension] + 'px';
			log(
				messageData.id,
				'IFrame (' + iframeId +
				') ' + dimension +
				' set to ' + messageData[dimension] + 'px'
			);
		}

		function chkZero(dimension){
			//FireFox sets dimension of hidden iFrames to zero.
			//So if we detect that set up an event to check for
			//when iFrame becomes visible.

			/* istanbul ignore next */  //Not testable in PhantomJS
			if (!hiddenCheckEnabled && '0' === messageData[dimension]){
				hiddenCheckEnabled = true;
				log(iframeId,'Hidden iFrame detected, creating visibility listener');
				fixHiddenIFrames();
			}
		}

		function processDimension(dimension){
			setDimension(dimension);
			chkZero(dimension);
		}

		var iframeId = messageData.iframe.id;

		if(settings[iframeId]){
			if( settings[iframeId].sizeHeight) { processDimension('height'); }
			if( settings[iframeId].sizeWidth ) { processDimension('width'); }
		}
	}

	function syncResize(func,messageData,doNotSync){
		/* istanbul ignore if */  //Not testable in PhantomJS
		if(doNotSync!==messageData.type && requestAnimationFrame){
			log(messageData.id,'Requesting animation frame');
			requestAnimationFrame(func);
		} else {
			func();
		}
	}

	function trigger(calleeMsg, msg, iframe, id, noResponseWarning) {
		function postMessageToIFrame(){
			var target = settings[id].targetOrigin;
			log(id,'[' + calleeMsg + '] Sending msg to iframe['+id+'] ('+msg+') targetOrigin: '+target);
			iframe.contentWindow.postMessage( msgId + msg, target );
		}

		function iFrameNotFound(){
			warn(id,'[' + calleeMsg + '] IFrame('+id+') not found');
		}

		function chkAndSend(){
			if(iframe && 'contentWindow' in iframe && (null !== iframe.contentWindow)){ //Null test for PhantomJS
				postMessageToIFrame();
			} else {
				iFrameNotFound();
			}
		}

		function warnOnNoResponse() {

			function warning() {
				warn(id, 'No response from iFrame. Check iFrameResizer.contentWindow.js has been loaded in iFrame');
			}

			if (!!noResponseWarning) {
				settings[id].msgTimeout = setTimeout(warning, settings[id].warningTimeout);

			}
		}


		id = id || iframe.id;

		if(settings[id]) {
			chkAndSend();
			warnOnNoResponse();
		}

	}

	function createOutgoingMsg(iframeId){
		return iframeId +
			':' + settings[iframeId].bodyMarginV1 +
			':' + settings[iframeId].sizeWidth +
			':' + settings[iframeId].log +
			':' + settings[iframeId].interval +
			':' + settings[iframeId].enablePublicMethods +
			':' + settings[iframeId].autoResize +
			':' + settings[iframeId].bodyMargin +
			':' + settings[iframeId].heightCalculationMethod +
			':' + settings[iframeId].bodyBackground +
			':' + settings[iframeId].bodyPadding +
			':' + settings[iframeId].tolerance +
			':' + settings[iframeId].inPageLinks +
			':' + settings[iframeId].resizeFrom +
			':' + settings[iframeId].widthCalculationMethod;
	}

	function setupIFrame(iframe,options){
		function setLimits(){
			function addStyle(style){
				if ((Infinity !== settings[iframeId][style]) && (0 !== settings[iframeId][style])){
					iframe.style[style] = settings[iframeId][style] + 'px';
					log(iframeId,'Set '+style+' = '+settings[iframeId][style]+'px');
				}
			}

			function chkMinMax(dimension){
				if (settings[iframeId]['min'+dimension]>settings[iframeId]['max'+dimension]){
					throw new Error('Value for min'+dimension+' can not be greater than max'+dimension);
				}
			}

			chkMinMax('Height');
			chkMinMax('Width');

			addStyle('maxHeight');
			addStyle('minHeight');
			addStyle('maxWidth');
			addStyle('minWidth');
		}

		function newId(){
			var id = ((options && options.id) || defaults.id + count++);
			if  (null !== document.getElementById(id)){
				id = id + count++;
			}
			return id;
		}

		function ensureHasId(iframeId){
			logId=iframeId;
			if (''===iframeId){
				iframe.id = iframeId =  newId();
				logEnabled = (options || {}).log;
				logId=iframeId;
				log(iframeId,'Added missing iframe ID: '+ iframeId +' (' + iframe.src + ')');
			}


			return iframeId;
		}

		function setScrolling(){
			log(iframeId,'IFrame scrolling ' + (settings[iframeId].scrolling ? 'enabled' : 'disabled') + ' for ' + iframeId);
			iframe.style.overflow = false === settings[iframeId].scrolling ? 'hidden' : 'auto';
			switch(settings[iframeId].scrolling) {
				case true:
					iframe.scrolling = 'yes';
					break;
				case false:
					iframe.scrolling = 'no';
					break;
				default:
					iframe.scrolling = settings[iframeId].scrolling;
			}
		}

		//The V1 iFrame script expects an int, where as in V2 expects a CSS
		//string value such as '1px 3em', so if we have an int for V2, set V1=V2
		//and then convert V2 to a string PX value.
		function setupBodyMarginValues(){
			if (('number'===typeof(settings[iframeId].bodyMargin)) || ('0'===settings[iframeId].bodyMargin)){
				settings[iframeId].bodyMarginV1 = settings[iframeId].bodyMargin;
				settings[iframeId].bodyMargin   = '' + settings[iframeId].bodyMargin + 'px';
			}
		}

		function checkReset(){
			// Reduce scope of firstRun to function, because IE8's JS execution
			// context stack is borked and this value gets externally
			// changed midway through running this function!!!
			var
				firstRun           = settings[iframeId].firstRun,
				resetRequertMethod = settings[iframeId].heightCalculationMethod in resetRequiredMethods;

			if (!firstRun && resetRequertMethod){
				resetIFrame({iframe:iframe, height:0, width:0, type:'init'});
			}
		}

		function setupIFrameObject(){
			if(Function.prototype.bind){ //Ignore unpolyfilled IE8.
				settings[iframeId].iframe.iFrameResizer = {

					close        : closeIFrame.bind(null,settings[iframeId].iframe),

					resize       : trigger.bind(null,'Window resize', 'resize', settings[iframeId].iframe),

					moveToAnchor : function(anchor){
						trigger('Move to anchor','moveToAnchor:'+anchor, settings[iframeId].iframe,iframeId);
					},

					sendMessage  : function(message){
						message = JSON.stringify(message);
						trigger('Send Message','message:'+message, settings[iframeId].iframe, iframeId);
					}
				};
			}
		}

		//We have to call trigger twice, as we can not be sure if all
		//iframes have completed loading when this code runs. The
		//event listener also catches the page changing in the iFrame.
		function init(msg){
			function iFrameLoaded(){
				trigger('iFrame.onload', msg, iframe, undefined , true);
				checkReset();
			}

			addEventListener(iframe,'load',iFrameLoaded);
			trigger('init', msg, iframe, undefined, true);
		}

		function checkOptions(options){
			if ('object' !== typeof options){
				throw new TypeError('Options is not an object');
			}
		}

		function copyOptions(options){
			for (var option in defaults) {
				if (defaults.hasOwnProperty(option)){
					settings[iframeId][option] = options.hasOwnProperty(option) ? options[option] : defaults[option];
				}
			}
		}

		function getTargetOrigin (remoteHost){
			return ('' === remoteHost || 'file://' === remoteHost) ? '*' : remoteHost;
		}

		function processOptions(options){
			options = options || {};
			settings[iframeId] = {
				firstRun	: true,
				iframe		: iframe,
				remoteHost	: iframe.src.split('/').slice(0,3).join('/')
			};

			checkOptions(options);
			copyOptions(options);

			settings[iframeId].targetOrigin = true === settings[iframeId].checkOrigin ? getTargetOrigin(settings[iframeId].remoteHost) : '*';
		}

		function beenHere(){
			return (iframeId in settings && 'iFrameResizer' in iframe);
		}

		var iframeId = ensureHasId(iframe.id);

		if (!beenHere()){
			processOptions(options);
			setScrolling();
			setLimits();
			setupBodyMarginValues();
			init(createOutgoingMsg(iframeId));
			setupIFrameObject();
		} else {
			warn(iframeId,'Ignored iFrame, already setup.');
		}
	}

	function debouce(fn,time){
		if (null === timer){
			timer = setTimeout(function(){
				timer = null;
				fn();
			}, time);
		}
	}

	/* istanbul ignore next */  //Not testable in PhantomJS
	function fixHiddenIFrames(){
		function checkIFrames(){
			function checkIFrame(settingId){
				function chkDimension(dimension){
					return '0px' === settings[settingId].iframe.style[dimension];
				}

				function isVisible(el) {
					return (null !== el.offsetParent);
				}

				if (isVisible(settings[settingId].iframe) && (chkDimension('height') || chkDimension('width'))){
					trigger('Visibility change', 'resize', settings[settingId].iframe, settingId);
				}
			}

			for (var settingId in settings){
				checkIFrame(settingId);
			}
		}

		function mutationObserved(mutations){
			log('window','Mutation observed: ' + mutations[0].target + ' ' + mutations[0].type);
			debouce(checkIFrames,16);
		}

		function createMutationObserver(){
			var
				target = document.querySelector('body'),

				config = {
					attributes            : true,
					attributeOldValue     : false,
					characterData         : true,
					characterDataOldValue : false,
					childList             : true,
					subtree               : true
				},

				observer = new MutationObserver(mutationObserved);

			observer.observe(target, config);
		}

		var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

		if (MutationObserver) createMutationObserver();
	}


	function resizeIFrames(event){
		function resize(){
			sendTriggerMsg('Window '+event,'resize');
		}

		log('window','Trigger event: '+event);
		debouce(resize,16);
	}

	/* istanbul ignore next */  //Not testable in PhantomJS
	function tabVisible() {
		function resize(){
			sendTriggerMsg('Tab Visable','resize');
		}

		if('hidden' !== document.visibilityState) {
			log('document','Trigger event: Visiblity change');
			debouce(resize,16);
		}
	}

	function sendTriggerMsg(eventName,event){
		function isIFrameResizeEnabled(iframeId) {
			return	'parent' === settings[iframeId].resizeFrom &&
					settings[iframeId].autoResize &&
					!settings[iframeId].firstRun;
		}

		for (var iframeId in settings){
			if(isIFrameResizeEnabled(iframeId)){
				trigger(eventName, event, document.getElementById(iframeId), iframeId);
			}
		}
	}

	function setupEventListeners(){
		addEventListener(window,'message',iFrameListener);

		addEventListener(window,'resize', function(){resizeIFrames('resize');});

		addEventListener(document,'visibilitychange',tabVisible);
		addEventListener(document,'-webkit-visibilitychange',tabVisible); //Andriod 4.4
		addEventListener(window,'focusin',function(){resizeIFrames('focus');}); //IE8-9
		addEventListener(window,'focus',function(){resizeIFrames('focus');});
	}


	function factory(){
		function init(options,element){
			function chkType(){
				if(!element.tagName) {
					throw new TypeError('Object is not a valid DOM element');
				} else if ('IFRAME' !== element.tagName.toUpperCase()) {
					throw new TypeError('Expected <IFRAME> tag, found <'+element.tagName+'>');
				}
			}

			if(element) {
				chkType();
				setupIFrame(element, options);
				iFrames.push(element);
			}
		}

		function warnDeprecatedOptions(options) {
			if (options && options.enablePublicMethods) {
				warn('enablePublicMethods option has been removed, public methods are now always available in the iFrame');
			}
		}

		var iFrames;

		setupRequestAnimationFrame();
		setupEventListeners();

		return function iFrameResizeF(options,target){
			iFrames = []; //Only return iFrames past in on this call

			warnDeprecatedOptions(options);

			switch (typeof(target)){
			case 'undefined':
			case 'string':
				Array.prototype.forEach.call(
					document.querySelectorAll( target || 'iframe' ),
					init.bind(undefined, options)
				);
				break;
			case 'object':
				init(options,target);
				break;
			default:
				throw new TypeError('Unexpected data type ('+typeof(target)+')');
			}

			return iFrames;
		};
	}

	function createJQueryPublicMethod($){
		if (!$.fn) {
			info('','Unable to bind to jQuery, it is not fully loaded.');
		} else if (!$.fn.iFrameResize){
			$.fn.iFrameResize = function $iFrameResizeF(options) {
				function init(index, element) {
					setupIFrame(element, options);
				}

				return this.filter('iframe').each(init).end();
			};
		}
	}

	if (window.jQuery) { createJQueryPublicMethod(window.jQuery); }

	if (typeof define === 'function' && define.amd) {
		define([],factory);
	} else if (typeof module === 'object' && typeof module.exports === 'object') { //Node for browserfy
		module.exports = factory();
	} else {
		window.iFrameResize = window.iFrameResize || factory();
	}

})();
